Sustainable growth in a front-end repo depends less on the tools you choose—and more on the architecture you put in place. It’s not enough to pick the “right tools”; success hinges on how well your system supports team autonomy, controls feature sprawl, and manages deployment complexity at scale.
In this article, I’ll walk through practical architectural patterns that have worked in production—and share a modular strategy for taming the chaos as your front-end surface area expands.
The Scaling Trap Most Teams Fall Into
Most front-end code-bases begin as a single, fast-moving app. But as the product matures and more teams come on board, that organic growth starts to break down. Without clear architectural boundaries, the code-base turns into a bottleneck. Here are the most common pain points:
- Feature Isolation: Teams struggle to build and ship independently without stepping on each other’s toes
- Code Reuse: Shared patterns—authentication flows, UI components, utility functions—get reimplemented across projects
- Deployment Complexity: A tightly-coupled architecture makes coordinated releases slow and risky
- Performance: Bundle sizes balloon as apps inherit more than they need
- Developer Experience: Onboarding becomes painful, with new devs needing weeks just to understand the sprawl
The Solution: A Three-Layer Monorepo Architecture
To scale without sacrificing maintainability, I designed a three-layer architecture that cleanly separates concerns while keeping the system cohesive. This structure supports rapid development, safe code reuse, and flexible deployment across teams and applications. Here’s the folder layout—and why it works:
front-end/
├── .changeset/
├── .devcontainer/
├── .docker/
├── .github/
├── .husky/
├── .vscode/
├── apps/ # Deployable Next.js applications
│ ├── e2e-testing/
│ ├── web/
│ └── storybook/
├── packages/ # Shared libraries and infrastructure
│ ├── css-config/
│ ├── eslint-config/
│ ├── i18n/
│ ├── prettier-config/
│ ├── routes/
│ ├── stylelint-config/
│ ├── ui/
│ └── utils/
├── features/ # Isolated business-domain features
│ ├── accounts/
│ ├── auth/
│ ├── dashboards/
│ ├── reports/
│ ├── search/
│ ├── settings/
│ └── example/
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.json
└── .... # Other root-level config and scripts
Layer 1: Applications (apps)
The applications layer contains complete, deployable applications:
Main Web Application (web)
This is the primary customer-facing application built with Next.js 15. It consumes shared packages and features but maintains its own identity and deployment cycle. The key insight here is that while it shares code with other parts of the system, it needs to be deployable independently.
Storybook (storybook/)
This serves as the component documentation and testing environment. Having it as a separate application rather than a package is crucial because it needs its own build process and can evolve independently of the main application.
End-to-End Testing (e2e-testing/)
This dedicated application for end-to-end testing ensures comprehensive testing coverage across the entire system without interfering with development workflows.
Layer 2: Shared Packages (packages)
This is where the magic happens. Shared code is organized into focused, single-responsibility packages:
Design System (ui)
A Vite-based React design system package that outputs ESM and CJS bundles. It includes a comprehensive design system with atomic design principles, accessibility patterns, and theme management. The decision to use Vite is driven by the need for fast development cycles and excellent TypeScript support.
Utility Library (utils)
Pure utilities with zero dependencies—math, string, date, validation helpers, react hooks, etc.
Internationalization (i18n)
The internationalization package is designed for type-safe, scoped translations using next-intl. Features own their namespaces (organized by feature + namespace), eliminating collisions and allowing seamless merging in the web app.
The i18n package supports:
- Locales scoped by feature + namespace
- Type safety with translation keys
- Common/shared namespace support
-
Automatic loading with
next-intl
Teams can own their translations without any naming collisions.
👉 Learn how to make i18n in Next.js fully type-safe
Routing (routes)
Provides type-safe routing helpers across features. Each feature defines its own routes, and the root app aggregates them—enabling modular navigation.
CSS Config (css-config)
A highly customizable styling configuration package that provides a fully themeable setup for Tailwind CSS and PostCSS. It supports multiple themes with full dark mode support out of the box, and includes utilities for defining and extending custom themes. Designed for scalability, it enables consistent theming across large front-end code-bases and is ideal for design systems and modular monorepos. The configuration is framework-agnostic and comes with comprehensive documentation for integration and theme customization.
👉 Learn more about @raminy/css-config
Configuration Packages
Linting and formatting rules are abstracted into reusable packages:
-
eslint-config -
prettier-config -
stylelint-config
Layer 3: Feature Modules (features)
Each feature is a self-contained Next.js application that can run on its own, complete with routing, translations, components, and business logic. It exports APIs to be consumed by the main app.
Every feature:
- Owns its routing and i18n
- Can be independently tested
-
Is composed into the
webapp with minimal coupling
This architecture allows feature teams to own their domain without being blocked by central coordination.
Why This Architecture Works: Principles and Objectives
After walking through the architecture, it’s important to step back and understand what it’s designed to achieve—and why the structure looks the way it does. This isn’t just about code organization. It’s about building a system that scales with your product, your teams, and your tooling needs.
Here are the key principles and outcomes that this architecture enables:
1. Reusable Building Blocks Across Repositories
While this monorepo houses a majority of the front-end work, it’s not assumed to be the only codebase. By publishing shared libraries (like css-config, stylelint-config, eslint-config, prettier-config, ui and utils) to npm, other independent repositories within the organization can consume and integrate these capabilities—ensuring a consistent look, feel, and behavior across systems.
2. Team Autonomy Through Feature Isolation
Features are implemented as self-contained modules that can:
- Run independently
- Own their routes, translations, and logic
- Be easily plugged into the main application
This enables teams to ship in parallel without stepping on each other’s toes.
3. Enforce Clear Separation of Concerns
Every package or feature focuses on a single problem:
- Shared concerns like styling, linting, formatting, and routing are centralized and abstracted into packages
- Application logic and domain features are isolated into their respective folders
This makes the code-base easier to reason about, debug, and extend.
4. Developer Experience That Scales with the Team
The architecture is designed for fast onboarding and productive workflows:
- Strict TypeScript enforcement and type-safe internationalization and routing
- Tailwind themes and PostCSS rules are codified in a reusable package
- DevContainer + Docker setups ensure consistent local environments
By isolating concerns, developers don’t have to navigate unnecessary context to build and ship confidently.
5. Safe, Transparent Versioning and Change Management
Changesets handle semantic versioning and changelog generation across all shared packages. This is augmented with custom scripts and a pre-commit hook that ensures:
- All modified projects include a corresponding changelog
- Internal dependencies are properly bumped
- Release workflows stay traceable and predictable
6. Prevention of Errors Before They Happen
A robust pre-commit hook (via Husky) blocks code if:
- TypeScript errors exist
- Formatting or linting rules are violated
- Required changesets are missing
This shifts error detection to the earliest possible stage—your editor.
7. Fast Builds and Targeted CI with Turbo and pnpm
Using pnpm workspaces and TurboRepo:
- Builds are incremental and cache-aware
- Dependencies are strictly controlled and deduplicated
- Only the affected packages are built and tested
This minimizes wasted CI time and accelerates feedback loops.
Core Tooling and What It Enables
A scalable architecture needs a solid foundation—not just in structure, but in the tooling that enforces consistency, automation, and developer experience. The tools chosen for this monorepo enable modular development, eliminate common sources of errors, and keep the system maintainable as it grows.
🧩 pnpm – Deterministic, Scalable Dependency Management
pnpm workspaces manage internal packages and feature modules efficiently. Its symlink-based approach:
- Prevents hidden or undeclared dependencies
- Speeds up installs via content-addressable storage
-
Enables real-time development via
workspace:*linking
Perfectly suited for large monorepos, where predictability and isolation are critical.
📝 Changesets – Automated Versioning and Changelog Management
Changesets manage semantic versioning across all publishable packages. Combined with custom pre-commit hooks, this setup:
- Forces every meaningful change to be documented
- Handles internal dependency bumping automatically
- Powers automated releases via GitHub Actions
No manual changelog wrangling or forgotten version bumps.
🪝 Husky – Guardrails at the Commit Level
Husky powers pre-commit hooks that enforce quality standards before code ever hits the main branch. It ensures:
- No TypeScript errors are committed
- All formatting and linting rules are respected
- Required changesets are in place for changed packages
This drastically reduces runtime issues and CI failures by catching problems early.
🌍 next-intl – Type-Safe, Modular Internationalization
Internationalization is handled via next-intl, wrapped in a custom i18n package that provides:
- Feature-scoped namespaces (with no collisions)
- Type-safe translation keys with full IDE autocomplete
- Shared "common" namespace for cross-cutting messages
- Locale resolution logic that works across features and apps
This setup enables each feature team to fully own their translations without central coordination.
🛠 DevContainer – Consistent Development Environments
The DevContainer setup provides a containerized, ready-to-go environment that includes:
- All tools (Node, pnpm, etc.) preinstalled
- Workspace mapping and editor integration
- Reproducible builds across machines and CI
Onboarding a new developer takes minutes—not hours.
⚡ TurboRepo – Fast, Dependency-Aware Builds
Turbo optimizes builds and tasks across the monorepo with:
- Incremental caching and change tracking
- Parallel execution across packages
- Intelligent skipping of unchanged builds/tests
🧹 Quality Configs (ESLint, Prettier, Stylelint)
All linting and formatting rules are encapsulated into shareable packages, ensuring consistency without duplication. They’re enforced via:
- Shared configs across all apps and packages
- Git hooks (via Husky)
- CI linting steps
No stylistic bikeshedding—just clean, uniform code.
🐳 Docker & Docker Compose – Unified Dev, Test, and Prod
Docker is used across development and production to:
- Run dev servers and test environments in containers
- Ensure builds match production behavior
- Support isolated environments for E2E testing and CI workflows
Multi-stage builds and volume mounts make it lightweight for local use.
🧪 Vitest, Storybook, and E2E Tools – Confidence at Every Layer
A multi-layered testing strategy ensures that bugs are caught early and consistently across all types of code:
- Unit Testing with Vitest for isolated logic, components, and utilities
- Integration Testing for validating interactions between features and packages
-
End-to-End Testing in a dedicated
e2e-testingapp using tools like Playwright or Cypress - Visual Regression Testing through Storybook integration to catch UI changes
Testing setup is tailored to the nature of the code being tested—whether it's deeply interactive UI, shared logic, or feature composition—ensuring quality without over-testing.
Final Thoughts
Scaling a front-end codebase is less about chasing the latest tools—and more about making intentional architectural decisions. By separating concerns, embracing modularity, and enforcing consistency through automation and type safety, this architecture creates an environment where teams can move quickly without creating chaos.
The three-layer structure outlined here balances flexibility with structure. It empowers teams to work independently, simplifies onboarding, and makes large-scale development predictable and sustainable.
Whether you're designing a monorepo from scratch or evolving an existing system, the principles behind this approach offer a clear path to long-term maintainability, performance, and developer satisfaction.
